/**
 * 
 */
package com.wgo.bpot.server.web.util;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;

import com.wgo.bpot.common.removeme.Persistent;
import com.wgo.bpot.common.transport.exception.db.DatabaseException;
import com.wgo.bpot.common.transport.util.Assert;
import com.wgo.bpot.common.transport.util.DomainModelTypeNamingConvention;
import com.wgo.bpot.common.transport.util.MethodNamingConvention;
import com.wgo.bpot.server.persist.trigger.CrudOperation;
import com.wgo.bpot.server.persist.trigger.TriggerRegistry;


/**
 * Converts ejb-instances to domain-model-instances. Links/relations are 
 * converted to customized level (determined by parameter).
 * 
 * @author petterei
 * @version $Id: ServerModelConverter.java,v 1.3 2006-01-26 08:36:30 oranvik Exp $
 */
public class ServerModelConverter {

	private static final transient Logger log = Logger.getLogger(ServerModelConverter.class);
	
	private static Map<Class<? extends Object>,Class<? extends Object>> modelToEjbTypeCache = new Hashtable<Class<? extends Object>,Class<? extends Object>>(10);	
	private static Map<Class, Class<Persistent>> ejbToModelTypeCache = new Hashtable<Class, Class<Persistent>>();

	/**
	 * Converts an ejb-instance to a domain-model-instance with all relations 
	 * intact. The objects referenced to is partial-instances.
	 * 
	 * @param ejbInstance The ejb-instance to be converted.
	 * @param triggerRegistry 
	 * @return The converted domain-model-instance.
	 */
	@SuppressWarnings("unchecked")
	public static Object convertToDomainModel(Persistent ejbInstance, int levels, Map<String, Object> cache, TriggerRegistry triggerRegistry) {
		if (null == ejbInstance || 0 > levels) {
			return null;
		}
		String cacheIdentifier = ejbInstance.getClass().getSimpleName()+ejbInstance.getDbId();
		if (cache.containsKey(cacheIdentifier)) {
			return cache.get(cacheIdentifier);
		}
		levels--;
		Class implClass = loadModelClass(ejbInstance.getClass());
		Persistent domainObject;
		try {
			domainObject = (Persistent)implClass.newInstance();
		} catch (Exception e) {
			String message = "Could not create DomainModel instance of type: " + implClass.getSimpleName() + " Source-type was: " + ejbInstance.getClass().getSimpleName();
			log.error(message, e);
			throw new DatabaseException(message, e);
		}
		cache.put(cacheIdentifier, domainObject);
		Class[] interfaces = implClass.getInterfaces();
		Map<String,Class> dispatchedMethods = new Hashtable<String,Class>();
		for (Class clazz : interfaces) {
			for (Method method : clazz.getMethods()) {
				MethodNamingConvention methodType = MethodNamingConvention.getMethodType(method);
				Method realGetter = methodType.retriveGetterMethod(implClass, method);
				Class methodReturnType = realGetter.getReturnType();
				Class existingValue = dispatchedMethods.put(realGetter.getName(), methodReturnType);
				if (null == existingValue || existingValue != methodReturnType) { // only set attributes once..
					Object ejbValue = ServerModelConverter.<Persistent>invokeMethod(ejbInstance, realGetter);
					Object argument = null;
					if (MethodNamingConvention.isActualLinkToConcept(realGetter)) {
						triggerRetrieve(triggerRegistry, ejbValue);
						argument = convertToDomainModel((Persistent)ejbValue, levels, cache,triggerRegistry);
					} else if (MethodNamingConvention.isActualLinkToConcepts(realGetter)) {
						Collection domainModelCollection = null;
						if (Set.class.isAssignableFrom(methodReturnType)) {
							domainModelCollection = new HashSet();
						} else if (List.class.isAssignableFrom(methodReturnType)) {
							domainModelCollection = new ArrayList();
						} else {
							String message = "Type " + ejbInstance.getClass().getSimpleName() + " has getter which returns collection of unknown type: " + methodReturnType.getSimpleName() + " This collections is ignored in the conversion.";
							log.warn(message);
							continue;
						}
						for (Persistent ejbRelationInstance : (Collection<Persistent>)ejbValue) {
							triggerRetrieve(triggerRegistry, ejbRelationInstance);
							Object linkResult = ServerModelConverter.convertToDomainModel(ejbRelationInstance, levels, cache,triggerRegistry);
							if (null != linkResult) {
								domainModelCollection.add(linkResult);
							}
						}
						argument = domainModelCollection;
					} else {
						argument = ejbValue;
					}
					Method realSetter = methodType.retriveSetterMethod(implClass, method);
//					log.info("Conversion, method dispatched: " + method.getName() + ", declaring interface: " + clazz.getSimpleName() + ", realGetter: " + realGetter.getName()+ ", realSetter: " + realSetter.getName());
					ServerModelConverter.<Persistent>invokeMethod(domainObject, realSetter, argument);
				}
			}
		}
		return domainObject;
	}

	private static void triggerRetrieve(TriggerRegistry triggerRegistry, Object ejbValue) {
		if (null != triggerRegistry) {
			triggerRegistry.invokeBefore(CrudOperation.RETRIEVE,ejbValue);
			triggerRegistry.invokeAfter(CrudOperation.RETRIEVE,ejbValue);
		}
	}

	@SuppressWarnings("unchecked")
	public static <T extends Object> T invokeMethod(Object instance, Method method, Object... args) {
		Assert.assertNotNull(instance, "The target instance of the invokation can not be null");
		Assert.assertNotNull(method, "The method to be invoked can not be null.");
		T result = null;
		try {
			result = (T)method.invoke(instance, args);
		} catch (Exception e) {
			String message = "Could not invoke method " 
				+ method.getName() + " on type " 
				+ instance.getClass().getSimpleName() + ", # parameters: " ;
			if (null != args) {
				message += args.length;
			} else {
				message += "'null'";
			}
			log.error(message, e);
			throw new DatabaseException(message, e);
		}
		return result;
	}
	
	@SuppressWarnings("unchecked")
	public static <T extends Object> Class<T> loadServerClass(Class<T> domainModelInterface) {
		if (modelToEjbTypeCache.containsKey(domainModelInterface)) {
			return (Class<T>)modelToEjbTypeCache.get(domainModelInterface);
		}
		Class<T> implClass = DomainModelTypeNamingConvention.loadServerClass(domainModelInterface);//, ServerModelNamingConvention.MODEL_IMPL_PACKAGE, ServerModelNamingConvention.MODEL_TYPE_SUFFIX);
		modelToEjbTypeCache.put(domainModelInterface,implClass);
		return implClass;
	}
	
	@SuppressWarnings("unchecked")
	public static Class<Persistent> loadModelClass(Class ejbClass) {
		if (ejbToModelTypeCache.containsKey(ejbClass)) {
			return ejbToModelTypeCache.get(ejbClass);
		}
		Class apiClass = DomainModelTypeNamingConvention.getImplClassFromEjbClass(ejbClass);
		ejbToModelTypeCache.put(ejbClass,apiClass);
		return apiClass;
	}
}
